
/*
 * Copyright (C) 2010 Bitglue. All rights reserved.
 *
 * $Id: BundleController.java bself $
 */

package com.bitglue.web.admin.webmvc;

import com.bitglue.web.admin.util.TransformUtils;
import com.bitglue.web.admin.util.XmlTreeUtils;
import com.bitglue.web.admin.webmvc.model.BundleModel;

import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.Date;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.servlet.ModelAndView;

import org.w3c.dom.Element;

/**
 * BundleController manages OSGi framework bundles.
 * @author Bryan Patrick Self
 */
@Controller("bundleController")
@SessionAttributes(types = BundleModel.class)
public class BundleController {

    private static final String[] PKG_HDRS =  {
      "Import-Package", "Export-Package",
      "Private-Package", "Ignore-Package",
      "DynamicImport-Package", "Include-Resource",
    };
    protected final Logger log = Logger.getLogger(getClass());

    @Autowired
    @Qualifier("bundleContext")
    private BundleContext bundleContext;

    @RequestMapping(value = "/bundles.do", method = RequestMethod.GET)
    public ModelAndView bundleSetup(HttpServletRequest req) {
        log.debug("creating new bundle model for client "
          + req.getRemoteAddr());
        BundleModel bundleModel = new BundleModel();
        List<BundleModel> bundles = new LinkedList<BundleModel>();
        for (Bundle b : bundleContext.getBundles()) {
            BundleModel bm = new BundleModel();
            bm.setState(bundleStateToString(b.getState()));
            bm.setId(b.getBundleId());
            bm.setLocation(b.getLocation().replaceAll(".*/", ""));
            bm.setSymbolicName(b.getSymbolicName());
            bm.setLastModified(new Date(b.getLastModified()));
            bm.setVersion(b.getVersion().toString());
            bm.setIsFragment(b.getHeaders().get(
              Constants.FRAGMENT_HOST) != null);
            bundles.add(bm);
        }
        bundleModel.setBundles(bundles);
        return new ModelAndView("bundle", "bundleModel", bundleModel);
    }

    @RequestMapping(value = "/getBundleHdrs.do")
    public void getBundleHeaders(@RequestParam("ID") long bundleId,
      @ModelAttribute BundleModel model, HttpServletResponse resp)
      throws Exception {
        resp.setContentType("text/xml");
        OutputStream os = resp.getOutputStream();
        for (Bundle b : bundleContext.getBundles()) {
            if (b.getBundleId() == bundleId) {
                log.debug("retrieving bundle headers for "
                  + "bundleId " + bundleId);
                buildTree(os, b.getHeaders());
                return;
            }
        }
        log.debug("failed to get bundle headers for bundle "
          + bundleId + " since it was not found");
        buildTree(os, new Hashtable());
    }

    /**
     * bundleOperation() executes a bundle operation
     * consisting of START, STOP, and EJECT.
     */
    @RequestMapping(value = "/bundleOp.do")
    public void bundleOperation(@RequestParam("OP") String op,
      @RequestParam("ID") int id, HttpServletResponse resp)
      throws Exception {
        log.info(op + " on bundle id " + id + " transpiring");
        Bundle target = null;
        for (Bundle b : bundleContext.getBundles()) {
            if (b.getBundleId() == id) {
                target = b;
                break;
            }
        }
        PrintWriter pw = new PrintWriter(resp.getOutputStream());
        try {
            if (target == null) {
                throw new BundleException("failed to perform bundle op "
                  + op + " because bundle id " + id + " not found");
            }
            if ("STOP".equals(op))
                target.stop();
            else if ("START".equals(op))
                target.start();
            else if ("EJECT".equals(op))
                target.uninstall();
            else
                throw new BundleException("unknown operation: " + op);
            log.debug("bundle op " + op + " on id " + id + " complete");
            pw.print("SUCCESS: bundle id " + id + " " + op + " complete");

        } catch (BundleException be) {
            log.debug(be);
            pw.println("FAIL: bundle op failed->" + be);
        } finally {
            pw.flush();
            pw.close();
        }
    }

    @SuppressWarnings("unchecked")
    private void buildTree(OutputStream os, Dictionary hdrs)
      throws Exception {
        int id = 1;
        Element root = XmlTreeUtils.newTree();
        for (Enumeration e = hdrs.keys(); e.hasMoreElements();) {
            String key = (String)e.nextElement();
            String val = (String)hdrs.get(key);
            if (!isPackageHdr(key)) {
                XmlTreeUtils.newLeaf(root, id++, key + " : " + val);
                continue;
            }
            String[] escPkgs = escapePkgs(val).split(",");
            Element par = XmlTreeUtils.newFolder(root, id++, key + " :");
            for (String pkg : escPkgs) {
                String[] mods = pkg.split(";");
                if (mods.length <= 1) {
                    XmlTreeUtils.newLeaf(par, id++, mods[0]);
                    continue;
                } else
                    id = appendMods(par, mods, id);
            }
        }
        TransformUtils.transform(root, os);
    }

    private boolean isPackageHdr(String key) {
        for (String ph : PKG_HDRS) {
            if (ph.equals(key))
                return true;
        }
        return false;
    }

    private String escapePkgs(String pkgs) {
        boolean inQuote = false;
        StringBuilder sb = new StringBuilder();
        for (int x = 0; x < pkgs.length(); x++) {
            try {
                char ch = pkgs.charAt(x);
                if (ch == '\"')
                    inQuote = !inQuote;
                ch = (ch == ',' && inQuote) ? '\t' : ch;
                sb.append(ch);
            } catch (IndexOutOfBoundsException ex) {
                continue;
            }
        }
        return sb.toString();
    }

    private int appendMods(Element par, String[] mods, int id) {
        Element sp = XmlTreeUtils.newFolder(par, id++, mods[0]);
        for (int x = 1; x < mods.length; x++) {
            if (!mods[x].startsWith("uses:=")) {
                mods[x] = mods[x].replaceAll("[\\t]", ",");
                XmlTreeUtils.newLeaf(sp, id++, mods[x]);
                continue;
            } else {
                Element up = XmlTreeUtils.newFolder(sp, id++, "uses :=");
                for (String st : mods[x].split("[\\t]")) {
                    String s = st.replaceAll("(^uses:=\")|([\"]$)", "");
                    XmlTreeUtils.newLeaf(up, id++, s);
                }
            }
        }
        return id;
    }

    private String bundleStateToString(int bundleState) {
        switch (bundleState) {
            case Bundle.UNINSTALLED: return "UNINSTALLED";
            case Bundle.INSTALLED: return "INSTALLED";
            case Bundle.RESOLVED: return "RESOLVED";
            case Bundle.STARTING: return "STARTING";
            case Bundle.STOPPING: return "STOPPING";
            case Bundle.ACTIVE: return "ACTIVE";
            default: return "UNKNOWN";
        }
    }

}
